package org.groovyflow.util

/**
 * @author Chuck Tassoni
 */
//@Typed
class Decision {
  //You can choose to pass rules into <code>match()</code> instead of adding rules to this member
   List<Rule> rules = new ArrayList<Rule>()

  //When using //@Typed these become truly private, which is what I want
  private def argsForPred
  private def argsForAction

  private Closure tempIfClosure

  private boolean okToThrowExceptionWhenNoMatch = true
  Closure transform = { returnedFromPredicate ->
    returnedFromPredicate instanceof List ? returnedFromPredicate :
      [returnedFromPredicate]
  }
  Closure makeFinalArgsForAction = {actionArgsSoFar, List argsReturnedsFromPred ->
    if (!(actionArgsSoFar instanceof List)) {
      def temp = []
      temp << actionArgsSoFar
      temp = temp +  argsReturnedsFromPred
      return temp
    } else {
      return ((List) actionArgsSoFar) + argsReturnedsFromPred
    }
  }

  public Decision okToThrowExceptionWhenNoMatch(boolean b) {
    okToThrowExceptionWhenNoMatch = b
    this
  }

  public Decision safe(){
    okToThrowExceptionWhenNoMatch(false)
  }

  public Decision setPredicateDelegate(Object obj) {
    rules.each {Rule rule -> rule.predicateDelegate = obj}
    this
  }

  public Decision setActionDelegate(Object obj) {
    rules.each {Rule rule -> rule.actionDelegate = obj}
    this
  }

  public Decision setDelegate(Object obj) {
    rules.each {Rule rule -> rule.delegate = obj}
    this
  }


  /**
   * Adds the arguments to be sent to Rule predicates and actions.  Note that you may choose to have your
   * Rule predicates and actions access closure scoped variables only, in which case you won't need to use
   * this method.
   * @param predArgs the arguments (if you pass a list) or argument to be sent to each Rule predicate.
   * @param actionArgs the arguments (if you pass a list) or argument to be sent to each Rule action.  If you
   * don't provide actionArgs, we'll assume they're the same as the predicate args.
   * @return this , so that you can chain this method, as in
   * <code>new Decision().withArgs([1,2])</code>
   */
  public Decision withArgs(predArgs, actionArgs = null) {
    if (actionArgs == null) actionArgs = predArgs
    this.argsForPred = predArgs
    this.argsForAction = actionArgs
    this
  }

  /**
   * Runs through each Rule until we find a match.  You have the choice of supplying rules by
   * passing them as an argument to this method or by adding to our 'rules' member variable, with the
   * former taking precedence (and the member Rule list not being used at all in this invocation).
   * <p>
   * We consider a Rule has been matched when the Rule's predicate closure returns something other than false or null.
   * The value returned by the predicate is then available to the action.  If the predicate returns a list,
   * the individual elements of the list are available as parameters of the action. For example,
   * when a predicate returns [x,y,z], the associated predicate might look like this:
   *{x,y,z ->  etc.} .  But note that the predicate can also decide not to declare all the variables,
   * and so {x,y -> etc.} would also e a valid declaration.
   * <p>
   * If a predicate only wishes to return a single scalar value, it needn't put that value in a list-- Rules
   * will do the work.  If you've provided arguments via the <code>withArgs</code> method before invoking this method,
   * then the value returned by the predicate is available to the action only after all the arguments passed into
   * withArgs() have been declared.
   *
   * @param rules If this is null we'll use the member variable of the same name
   * @return the result of the Rules match
   */
  def apply(Rule... rules = null) {
    List theRules = rules == null ? this.rules : rules
    for (aRule in theRules) {
      def decisionBasis = ClosureHelp.invokeClosure(aRule.pred, argsForPred)
      if (decisionBasis == null || (decisionBasis instanceof Boolean && !decisionBasis) )
        continue;
      def transformed = transform(decisionBasis)
      if (argsForAction == null)
        return ClosureHelp.invokeClosure(aRule.action, transformed)
      else {
        return ClosureHelp.invokeClosure(aRule.action, makeFinalArgsForAction(argsForAction, transformed))
      }
    }
    if (okToThrowExceptionWhenNoMatch)
      throw new NoMatchException()
  }



  public static Rule rule(Closure pred, Closure action){
    return new Rule(pred, action)
  }

  public static Rule DoNothingRule(){
    rule({true}, {})
  }

  public static Closure toIf(Closure pred, Closure action){
    Closure clos = {new Decision().okToThrowExceptionWhenNoMatch(false).apply(new Rule(pred, action)) }
    clos
  }

  public static toIfResult(Closure pred, Closure action){
     toIf(pred, action).call()
  }

  public Decision "if"(Closure clos){
     tempIfClosure = clos
     this
  } 

  public Decision "do"(Closure clos){
    assert tempIfClosure != null : "Attempt to assign a 'do' when no 'if' exists"
    rules << new Rule(tempIfClosure, clos)
    this
  }

 // TODO Allow a runall_mode that runs doesn't stop processing when a Rule is matched.  For example,
 // if rule1 was matched, rule2 wasn't matched, and rule3 was matched, in runall_mode we'd run
 // both rule1 and rule3.  The default behavior would be our current behavior, which of course would
 // only run rule1.

}